package com.binance.basedao.mybatis;

import java.net.InetAddress;
import java.util.Calendar;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * <b>Description:</b>〈ID生成器,copy from 'io.shardingsphere.core.keygen.DefaultKeyGenerator'〉<br/>
 * Default distributed primary key generator.
 *
 * <p>
 * Use snowflake algorithm. Length is 64 bit.
 * </p>
 *
 * <pre>
 * 1bit   sign bit.
 * 41bits timestamp offset from 2016.11.01(Sharding-Sphere distributed primary key published data) to now.
 * 10bits worker process id.
 * 12bits auto increment offset in one mills
 * </pre>
 *
 * <p>
 * Call @{@code DefaultKeyGenerator.setWorkerId} to set.
 * </p>
 * @author hongchaoMao <br/>
 *         Create date: 2018/11/27
 * @version v1.0.0
 */
public class KeyGenerator {

    private static final Logger logger = Logger.getLogger(KeyGenerator.class.getName());

    private static long EPOCH = 0L;

    private static final long SEQUENCE_BITS = 12L;

    private static final long WORKER_ID_BITS = 10L;

    private static final long SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1;

    private static final long WORKER_ID_LEFT_SHIFT_BITS = SEQUENCE_BITS;

    private static final long TIMESTAMP_LEFT_SHIFT_BITS = WORKER_ID_LEFT_SHIFT_BITS + WORKER_ID_BITS;

    private static final long WORKER_ID_MAX_VALUE = 1L << WORKER_ID_BITS;

    private static long workerId;

    private long sequence;

    private long lastTime;

    private KeyGenerator(){
        Calendar calendar = Calendar.getInstance();
        calendar.set(2018, Calendar.NOVEMBER, 1);
        calendar.set(Calendar.HOUR_OF_DAY, 0);
        calendar.set(Calendar.MINUTE, 0);
        calendar.set(Calendar.SECOND, 0);
        calendar.set(Calendar.MILLISECOND, 0);
        EPOCH = calendar.getTimeInMillis();
        // set workId
        KeyGenerator.setWorkerId(genWorkerIdFromIp());
    }

    private static KeyGenerator keyGenerator;

    public static KeyGenerator getInstance(){
        if (keyGenerator == null) {
            synchronized (KeyGenerator.class) {
                if (keyGenerator == null) {
                    keyGenerator = new KeyGenerator();
                }
            }
        }
        return keyGenerator;
    }

    /**
     * Set work process id.
     *
     * @param workerId work process id
     */
    private static void setWorkerId(final long workerId) {
        if (!(workerId >= 0L && workerId < WORKER_ID_MAX_VALUE)) {
            throw new IllegalArgumentException();
        }
        KeyGenerator.workerId = workerId;
    }

    /**
     * Generate work process id.
     *
     * @return workerId is @{@link Long}.
     */
    private static long genWorkerIdFromIp() {
        InetAddress address = null;
        try {
            address = InetAddress.getLocalHost();
        } catch (Exception ex) {
            logger.log(Level.WARNING, "------>genWorkerIdFromIp() Get local ip address failed, error: " + ex);
        }
        byte[] byteArray;
        if (address != null) {
            byteArray = address.getAddress();
        } else {
            byteArray = Thread.currentThread().getName().getBytes();
        }
        int ipAddressLength = byteArray.length;
        long workerId = (long)(((byteArray[ipAddressLength - 2] & 3) << 8)
                + (byteArray[ipAddressLength - 1] & 255));
        workerId &= 1023L;
        return workerId;
    }
    /**
     * Generate key.
     *
     * @return key type is @{@link Long}.
     */
    public synchronized Number generateKey() {
        long currentMillis = getCurrentMillis();
        if(lastTime > currentMillis) {
            throw new IllegalStateException(String.format("Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds",
                    lastTime, currentMillis));
        }
        if (lastTime == currentMillis) {
            if (0L == (sequence = (sequence + 1) & SEQUENCE_MASK)) {
                currentMillis = waitUntilNextTime(currentMillis);
            }
        } else {
            sequence = 0;
        }
        lastTime = currentMillis;
        return ((currentMillis - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (workerId << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
    }

    private long waitUntilNextTime(final long lastTime) {
        long time = getCurrentMillis();
        while (time <= lastTime) {
            time = getCurrentMillis();
        }
        return time;
    }

    /**
     * Get current millis.
     *
     * @return current millis
     */
    private long getCurrentMillis() {
        return System.currentTimeMillis();
    }

}
